Skip to content
This repository was archived by the owner on Oct 16, 2020. It is now read-only.

Support TS plugins #327

Merged
merged 16 commits into from
Sep 29, 2017
Merged

Conversation

tomv564
Copy link
Contributor

@tomv564 tomv564 commented Aug 6, 2017

An investigation for #326

There are two ways of specifying plugins to be loaded:

  1. plugins declaration in tsconfig.json
  2. --globalPlugins argument to tsserver

There are a few different ways to load the plugin:

  1. locally installed with the project (as sibling to typescript itself)
  2. global npm package
  3. Locations provided via --pluginProbeLocations

This PR contains implementation for loading a locally installed plugin specified in tsconfig.json only, as documented on Erich Gamma's new vscode plugin at https://github.com/Microsoft/vscode-ts-tslint.

screen shot 2017-08-06 at 20 49 42

Missing:

  • Review what parts of PluginCreateInfo need to be wired up for other plugins
  • Support for globalPlugins and pluginProbeLocations
  • A good way to disable loading plugins altogether for sourcegraph
  • Tests

@codecov
Copy link

codecov bot commented Aug 6, 2017

Codecov Report

Merging #327 into master will decrease coverage by 0.09%.
The diff coverage is 69.73%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master     #327     +/-   ##
=========================================
- Coverage    74.8%   74.71%   -0.1%     
=========================================
  Files          14       15      +1     
  Lines        1691     1760     +69     
  Branches      317      331     +14     
=========================================
+ Hits         1265     1315     +50     
- Misses        280      294     +14     
- Partials      146      151      +5
Impacted Files Coverage Δ
src/match-files.ts 66.17% <100%> (+0.16%) ⬆️
src/diagnostics.ts 71.42% <100%> (+2.19%) ⬆️
src/typescript-service.ts 73.06% <100%> (+0.49%) ⬆️
src/project-manager.ts 81.95% <38.46%> (-1.81%) ⬇️
src/plugins.ts 74.57% <74.57%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9c7cd41...42ce6e8. Read the comment docs.

@felixfbecker
Copy link
Contributor

Could you add doc comments to all the symbols you added? Makes it easier to review

// TODO: add peer node_modules to source path.

// TODO: determine how to expose this setting.
// VS Code starts tsserver with --allowLocalPluginLoads by default: https://github.com/Microsoft/TypeScript/pull/15924
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be good to have a setting for through didChangeWorkspaceConfiguration, but it must be off by default or it would allow remote code execution.

@tomv564
Copy link
Contributor Author

tomv564 commented Sep 10, 2017

Plugin support:
The angular and graphql plugins should get all they need from the PluginCreateInfo we provide (languageServer, languageServerHost and project.projectService.logger)

Plugin configuration:
I chose to read settings from initializationOptions as supporting didWorkspaceConfigChange would mean recreating the ProjectConfigurations - I believe plugin config would be unlikely to change after startup.

Plugin loading:
Plugins can be installed locally: i.e. project/node_modules/ts-plugin
Plugins can be installed globally, but the server will only pick them up if it is also installed globally.
Plugins can be manually pointed to using pluginProbeLocations.

Security:
If no flags are passed to initializationOptions, no plugin should be loaded unless the user has installed it as a peer package to javascript-typescript-langserver. Is this sufficient?

@tomv564
Copy link
Contributor Author

tomv564 commented Sep 17, 2017

@felixfbecker the plugins code was extracted and tests have been added, care to have a look?

* @param pluginModuleFactory function to create the module
* @param configEntry extra settings from tsconfig to pass to the plugin module
*/
private enableProxy(pluginModuleFactory: PluginModuleFactory, configEntry: ts.PluginImport) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be called multiple times to wrap with multiple plugins?

Copy link
Contributor Author

@tomv564 tomv564 Sep 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, every time enableProxy is called, the current this.service is wrapped, so the desired result is plugin2Wrapper(plugin1Wrapper(tsService))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A name like wrapService would make it more clearer to me then, enable always sounds like an idempotent function (like setting a boolean to true)

@@ -852,10 +864,39 @@ export class ProjectConfiguration {
this.logger
);
this.service = ts.createLanguageService(this.host, this.documentRegistry);
const pluginLoader = new PluginLoader(this.rootFilePath, this.fs, this.initializationOptions, this.logger);
pluginLoader.loadPlugins(options, this.enableProxy.bind(this));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use an arrow function, bind is not type safe

const pluginModule = pluginModuleFactory({ typescript: ts });
this.service = pluginModule.create(info);
} catch (e) {
this.logger.info(`Plugin activation failed: ${e}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be an error

src/plugins.ts Outdated
import { combinePaths } from './match-files';
import { InitializationOptions } from './request-type';

// definitions from from TypeScript server/project.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add a full link to GitHub (with revision)

@felixfbecker
Copy link
Contributor

Plugin configuration:
I chose to read settings from initializationOptions as supporting didWorkspaceConfigChange would mean recreating the ProjectConfigurations - I believe plugin config would be unlikely to change after startup.

initializationOptions is not specced to contain the settings, it is typed as any. You should use didWorkspaceConfigChange, but you can filter it to only recreate ProjectConfigurations

public loadPlugins(options: ts.CompilerOptions, applyProxy: EnableProxyFunc) {
// Search our peer node_modules, then any globally-specified probe paths
// ../../.. to walk from X/node_modules/javascript-typescript-langserver/lib/project-manager.js to X/node_modules/
const searchPaths = [combinePaths(__filename, '../../..'), ...this.pluginProbeLocations];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the difference between combinePaths and path.resolve()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code is copied from TS project, wanted to keep it as close to original as possible (and we had the function already)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that wasn't clear to me. Maybe change the wording "based on" at the top of the file to "copied from"?

}

// Provide global: true so plugins can detect why they can't find their config
this.enablePlugin({ name: globalPluginName, global: true } as ts.PluginImport, searchPaths, applyProxy);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is global not in the typings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code is copied from TS project - fix should be made there

*/
private enablePlugin(pluginConfigEntry: ts.PluginImport, searchPaths: string[], enableProxy: EnableProxyFunc) {
for (const searchPath of searchPaths) {
const resolvedModule = this.resolveModule(pluginConfigEntry.name, searchPath) as PluginModuleFactory;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this cast needed / why is resolveModule not typed with that return type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveModule was not specific to plugin loading, it also was copied and I tried to keep it close to the original.
If it is exposed through the TS api we could remove it from this plugin.ts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an open issue for exposing this? If not, could you create one and link it at the top?

src/plugins.ts Outdated
private resolveJavaScriptModule(moduleName: string, initialDir: string, host: ts.ModuleResolutionHost): string {
// TODO: this should set jsOnly=true to the internal resolver, but this parameter is not exposed on a public api.
const result =
ts.nodeModuleNameResolver(moduleName, /* containingFile */ initialDir.replace('\\', '/') + '/package.json', { moduleResolution: ts.ModuleResolutionKind.NodeJs, allowJs: true }, this.resolutionHost, undefined);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please wrap long lines by making one argument per line:

const foo = bar(
  baz,
  qux,
  qaz
)

@@ -852,10 +864,39 @@ export class ProjectConfiguration {
this.logger
);
this.service = ts.createLanguageService(this.host, this.documentRegistry);
const pluginLoader = new PluginLoader(this.rootFilePath, this.fs, this.pluginSettings, this.logger);
pluginLoader.loadPlugins(options, this.enableProxy.bind(this) /* (factory, config) => this.enableProxy(factory, config)) */);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't use bind(), it's not type safe. wrap in an arrow function instead

}
export type Settings = {
format: ts.FormatCodeSettings
} & PluginSettings;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer interface Settings extends PluginSettings { ... }

@felixfbecker
Copy link
Contributor

Good to merge once lint failure is fixed

@tomv564
Copy link
Contributor Author

tomv564 commented Sep 27, 2017

Thanks for the work you put into merging yesterday! I'll try to clear up any of the outstanding issues with Typescript and ping you when the branch has received the possible short-term fixes.

@tomv564
Copy link
Contributor Author

tomv564 commented Sep 29, 2017

Test is fixed, confirmed it still works on Windows too!
Too bad about the coverage stats, wrapService is untested but it's private and only called by successful plugin imports.

@felixfbecker felixfbecker merged commit 9cd02d0 into sourcegraph:master Sep 29, 2017
@tomv564 tomv564 deleted the feature-ts-plugins branch September 30, 2017 07:25
@Kronuz
Copy link

Kronuz commented Jun 13, 2018

As some plugins also receive configuration options themselves (configFile, ignoreDefinitionFiles, etc., for tslint-language-service), globalPlugins could be a list of objects, similarly to the tsconfig.json configuration for "compilerOptions" (globalPlugins could even be renamed to simply plugins to make it the same):

"plugins": [
  {
    "name": "tslint-language-service",
    "alwaysShowRuleFailuresAsWarnings": false,
    "ignoreDefinitionFiles": true,
    "configFile": "../tslint.json",
    "disableNoUnusedVariableRule": false,
    "supressWhileTypeErrorsPresent": false,
    "mockTypeScriptVersion": false
  }
]

I'd suggest a way to be able to pass those to the plugin maybe passing the whole object here, prior maybe adding global: true to it:

this.enablePlugin({ name: globalPluginName, global: true } as ts.PluginImport, searchPaths, applyProxy)

What do you think?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants